
#include "stdafx.h"
#include "TetrisApp.h"
#include <time.h>

// disable GDI+ warning: convertion from int to real
#pragma warning(disable: 4244)

using namespace Gdiplus;

CTetrisApp::Config::Config()
{
	itemWidth = 16;
	itemBoardSize = 2;
	
	colorBackground.SetFromCOLORREF(::GetSysColor(COLOR_BTNFACE));
	colorGrid = Color::DarkBlue;
	colorPreview = Color(128, 0, 0, 255);
	bShowGrid = true;

	gridPosition.X = 15;
	gridPosition.Y = 15;

	rectQueue = Rect(gridPosition.X + itemWidth * COL + 15, gridPosition.Y, 50, 50);
	rectLevel = Rect(rectQueue.X, rectQueue.Y + rectQueue.Height + 20, 50, 50);
	rectScore = Rect(rectLevel.X, rectLevel.Y + rectLevel.Height + 20, 50, 50);

	colorLevel = Color::Black;
	colorScore = Color::Black;

	FontFamily fontFamily(L"Verdana");
	pFontTitle = new Font(&fontFamily, 10, FontStyleRegular, UnitPixel);
	pFontText = new Font(&fontFamily, 10, FontStyleRegular, UnitPixel);
}

CTetrisApp::Config::~Config()
{
	delete pFontTitle;
	delete pFontText;
}

CTetrisApp::CTetrisApp(HWND hwnd, const Rect &clientRect, const Config *pCfg)
	: m_graphics(::GetDC(hwnd))
{
	m_curX = 0;
	m_curY = 0;
	memset(m_area, 0, sizeof(m_area));
	
	if (pCfg != NULL && IsConfigValid(pCfg)) {
		m_cfg = *pCfg;
	}

	m_hWnd = hwnd;
	m_clientRect = clientRect;

	m_bStarted = false;
	m_score = 0;
	m_level = 1;
	m_nTimerInterval = 500;

	srand(time(NULL));
}

void CTetrisApp::Start()
{
	m_score = 0;
	m_level = 1;
	m_bStarted = true;

	CreateNewShape();
	Paint();
	UpdateTimer();
	PlaySnd(IDR_WAVE_START);
}

void CTetrisApp::UpdateTimer()
{
	m_nTimerInterval = 100 + 500 / m_level;
	::KillTimer(m_hWnd, ID_TETRIS_APP);
	::SetTimer(m_hWnd, ID_TETRIS_APP, m_nTimerInterval, NULL);
}

void CTetrisApp::Paint()
{
	EraseBackground();
	DrawArea();
	DrawLevel();
	DrawScore();
	DrawShape(&m_shape, m_curX, m_curY, m_shape.GetColor());
	DrawPreview(&m_shape, m_cfg.colorPreview);
	DrawQueuedShape();
}

void CTetrisApp::OnKeydown(WPARAM wParam)
{
	switch (wParam) {
	case VK_SPACE:
		while (!IsReachBoard(&m_shape, m_curX, m_curY, CShape::dirSouth)) {
			MoveShape(&m_shape, CShape::dirSouth);
		}
		OnTimer();
		break;

	case VK_UP:
		RotateShape(&m_shape);
		break;

	case VK_DOWN:
		MoveShape(&m_shape, CShape::dirSouth);
		break;

	case VK_LEFT:
		MoveShape(&m_shape, CShape::dirWest);
		break;

	case VK_RIGHT:
		MoveShape(&m_shape, CShape::dirEast);
		break;
	}
}

void CTetrisApp::DrawGrid()
{
	Pen pen(m_cfg.colorGrid);
	Point p1, p2;
    int i;

	for (i = 0; i <= ROW; i++) {
    	p1.X = m_cfg.gridPosition.X;
		p1.Y = m_cfg.gridPosition.Y + m_cfg.itemWidth * i;
        p2.X = p1.X + m_cfg.itemWidth * COL;
        p2.Y = p1.Y;
		m_graphics.DrawLine(&pen, p1, p2);
    }

    for (i = 0; i <= COL; i++) {
    	p1.X = m_cfg.gridPosition.X + m_cfg.itemWidth * i;
        p1.Y = m_cfg.gridPosition.Y;
        p2.X = p1.X;
        p2.Y = p1.Y + m_cfg.itemWidth * ROW;
		m_graphics.DrawLine(&pen, p1, p2);
    }
}

void CTetrisApp::DrawPreview(CShape *pShape, Color color)
{
	int x = m_curX;
	int y = m_curY;

	while (!IsReachBoard(pShape, x, ++y, CShape::dirSouth));

	if (y - 1 != m_curY) {
		if (color.GetValue() == m_cfg.colorBackground.GetValue()) {
			ClearShape(pShape, x, y);
		}
		else {
			HatchBrush brush(HatchStyle30Percent, color, m_cfg.colorBackground);
			
			x = m_cfg.gridPosition.X + x * m_cfg.itemWidth;
			y = m_cfg.gridPosition.Y + y * m_cfg.itemWidth;

			DrawShape(
				pShape,
				x,
				y,
				m_cfg.itemWidth,
				m_cfg.itemBoardSize,
				color,
				brush
			);
		}
	}
}

void CTetrisApp::DrawShape(CShape *pShape, int ox, int oy, Color color)
{
	int itemWidth = m_cfg.itemWidth;
	int itemBoardSize = m_cfg.itemBoardSize;
	int x = ox * m_cfg.itemWidth + m_cfg.gridPosition.X;
	int y = oy * m_cfg.itemWidth + m_cfg.gridPosition.Y;
	
	if (color.GetValue() == m_cfg.colorBackground.GetValue()) {
		ClearShape(pShape, ox, oy);
	}
	else {
		SolidBrush brush(color);
		DrawShape(pShape, x, y, itemWidth, itemBoardSize, color, brush);
	}
}

void CTetrisApp::ClearShape(CShape *pShape, int ox, int oy)
{
	int itemWidth = m_cfg.itemWidth;
	int itemBoardSize = m_cfg.itemBoardSize;
	int x = ox * m_cfg.itemWidth + m_cfg.gridPosition.X;
	int y = oy * m_cfg.itemWidth + m_cfg.gridPosition.Y;

	SolidBrush brush(m_cfg.colorBackground);
	DrawShape(pShape, x, y, itemWidth, itemBoardSize, m_cfg.colorBackground, brush);
}

void CTetrisApp::DrawShape(CShape *pShape, int x, int y, int itemWidth, int itemBoardSize, Color color, const Brush& brush)
{
	int i, j;
	int _x, _y;

	for (i = 0; i < CShape::SHAPE_WIDTH; i++) {
		for (j = 0; j < CShape::SHAPE_WIDTH; j++) {
			_x = x + j * itemWidth;
			_y = y + i * itemWidth;
			
			if (pShape->m_buffer[i][j]) {
				Rect rect(
					_x + itemBoardSize,
					_y + itemBoardSize,
					itemWidth - itemBoardSize,
					itemWidth - itemBoardSize
				);
				
				m_graphics.FillRectangle(&brush, rect);
			}
		}
	}
}

void CTetrisApp::RotateShape(CShape *pShape, bool bLeft)
{
	CShape tmpShape = *pShape;
	tmpShape.Rotate(bLeft);

	int x = m_curX;
	int y = m_curY;

	if (IsOutofBoard(&tmpShape, x, y)) {
		for (int i = 1; i <= CShape::SHAPE_WIDTH; i++) {
			if (!IsOutofBoard(&tmpShape, x + i, y)) {
				x += i;
				break;
			}

			if (!IsOutofBoard(&tmpShape, x - i, y)) {
				x -= i;
				break;
			}
		}
	}

	if (!IsOutofBoard(&tmpShape, x, y)) {
		DrawShape(pShape, m_curX, m_curY, m_cfg.colorBackground);
		DrawPreview(pShape, m_cfg.colorBackground);
		*pShape = tmpShape;
		m_curX = x;
		m_curY = y;
		DrawShape(pShape, m_curX, m_curY, pShape->GetColor());
		DrawPreview(pShape, m_cfg.colorPreview);
	}
}

void CTetrisApp::MoveShape(CShape *pShape, int dir)
{
	if (IsReachBoard(pShape, m_curX, m_curY, dir))
		return;

	DrawPreview(pShape, m_cfg.colorBackground);
	DrawShape(pShape, m_curX, m_curY, m_cfg.colorBackground);

	switch(dir) {
	case CShape::dirNorth:
		m_curY--;
		break;

	case CShape::dirEast:
		m_curX++;
		break;

	case CShape::dirSouth:
		m_curY++;
		break;

	case CShape::dirWest:
		m_curX--;
		break;

	default:
		return;
	}

	DrawPreview(pShape, m_cfg.colorPreview);
	DrawShape(pShape, m_curX, m_curY, pShape->GetColor());
}

bool CTetrisApp::IsOutofBoard(CShape *pShape, int x, int y)
{
	int ax, ay;
	BYTE b;

	for (int i = 0; i < CShape::SHAPE_WIDTH; i++) {
		for (int j = 0; j < CShape::SHAPE_WIDTH; j++) {
			ax = j + x;
			ay = i + y;

			if (ax < 0 || ax >= COL || ay < 0 || ay >= ROW) {
				b = true;
			}
			else {
				b = m_area[ay][ax].used;
			}

			if (pShape->m_buffer[i][j] && b) {
				return true;
			}
		}
	}

	return false;
}

bool CTetrisApp::IsReachBoard(CShape *pShape, int x, int y, int dir)
{
	switch (dir) {
	case CShape::dirNorth:
		y--;
		break;

	case CShape::dirEast:
		x++;
		break;

	case CShape::dirSouth:
		y++;
		break;

	case CShape::dirWest:
		x--;
		break;

	default:
		return true;
	}

	return IsOutofBoard(pShape, x, y);
}

bool CTetrisApp::IsConfigValid(const Config *pCfg)
{
	if (pCfg->itemBoardSize > 0) {
		if (pCfg->itemBoardSize * 2 < pCfg->itemWidth) {
			if (pCfg->gridPosition.X > 0 && pCfg->gridPosition.Y > 0) {
				return true;
			}
		}
	}

	return false;
}

void CTetrisApp::EraseBackground()
{
	SolidBrush brush(m_cfg.colorBackground);

	// draw background
	m_graphics.FillRectangle(&brush, m_clientRect);
}

void CTetrisApp::DrawArea()
{
	int x, y;
	int i, j;
	Color color;

	Pen pen(m_cfg.colorGrid);
	int gridBoardWidth = 2;

	pen.SetWidth(gridBoardWidth);
	RectF rect(
		m_cfg.gridPosition.X - gridBoardWidth / 2,
		m_cfg.gridPosition.Y - gridBoardWidth / 2,
		m_cfg.itemWidth * COL + gridBoardWidth + 1,
		m_cfg.itemWidth * ROW + gridBoardWidth + 1
	);
	m_graphics.DrawRectangle(&pen, rect);

	if (m_cfg.bShowGrid) {
		DrawGrid();
	}

	for (i = ROW - 1; i >= 0; i--) {
		for (j = 0; j < COL; j++) {
			x = m_cfg.gridPosition.X + j * m_cfg.itemWidth;
			y = m_cfg.gridPosition.Y + i * m_cfg.itemWidth;

			if (!m_area[i][j].used)
				color = m_cfg.colorBackground;
			else
				color = m_area[i][j].color;

			SolidBrush brush(color);
			Rect rect(
				x + m_cfg.itemBoardSize,
				y + m_cfg.itemBoardSize,
				m_cfg.itemWidth - m_cfg.itemBoardSize,
				m_cfg.itemWidth - m_cfg.itemBoardSize
			);
			m_graphics.FillRectangle(&brush, rect);
		}
	}
}

void CTetrisApp::LevelUp()
{
	UpdateTimer();
	PlaySnd(IDR_WAVE_LEVELUP);
}

void CTetrisApp::UpdateLevel()
{
	int level = m_score / 100 + 1;
	if (level != m_level) {
		m_level = level;
		DrawLevel();
		LevelUp();
	}
}

void CTetrisApp::UpdateScore(int nCompleted)
{
	if (nCompleted > 0) {
		m_score += 10 * nCompleted * nCompleted ;
		DrawScore();
		PlaySnd(IDR_WAVE_DELETE);
		UpdateLevel();
	}
}

void CTetrisApp::UpdateArea()
{
	int x, y;
	int i, j;

	if (!IsReachBoard(&m_shape, m_curX, m_curY, CShape::dirSouth)) {
		return;
	}

	for (i = 0; i < CShape::SHAPE_WIDTH; i++) {
		y = m_curY + i;
		if (y >= 0 && y < ROW) {
			for (j = 0; j < CShape::SHAPE_WIDTH; j++) {
				x = m_curX + j;
				if (x >= 0 && x < COL) {
					if (m_shape.m_buffer[i][j]) {
						m_area[y][x].used = m_shape.m_buffer[i][j];
						m_area[y][x].color = m_shape.GetColor();
					}
				}
			}
		}
	}

	bool bCompleted;
	int row, nCompleted = 0;
	AreaItem tmpArea[ROW][COL];

	row = ROW - 1;
	memset(tmpArea, 0, sizeof(tmpArea));

	for (i = ROW - 1; i >= 0; i--) {
		bCompleted = true;
		for (j = 0; j < COL; j++) {
			if (!m_area[i][j].used) {
				bCompleted = false;
				break;
			}
		}

		if (!bCompleted) {
			memcpy(tmpArea[row], m_area[i], sizeof(tmpArea[row]));
			row--;
		}
		else {
			nCompleted++;
		}
	}

	PlaySnd(IDR_WAVE_BOTTOM);
	UpdateScore(nCompleted);
	memcpy(m_area, tmpArea, sizeof(m_area));
	DrawArea();
}

void CTetrisApp::OnTimer()
{
	if (m_bStarted) {
		if (IsReachBoard(&m_shape, m_curX, m_curY, CShape::dirSouth)) {
			UpdateArea();
			CreateNewShape();
		}
		else {
			MoveShape(&m_shape, CShape::dirSouth);
		}
	}
}

void CTetrisApp::CreateNewShape()
{
	m_curX = (COL - CShape::SHAPE_WIDTH) / 2;
	m_curY = 0;

	int i = m_shapesQ.size() == 0 ? 4 : 1;
	while (i--) {
		m_shapesQ.push(rand() % CShape::TYPE_COUNT);
	}
	m_shape.SetType(m_shapesQ.front());
	m_shapesQ.pop();
	DrawQueuedShape();

	if (IsOutofBoard(&m_shape, m_curX, m_curY)) {
		m_bStarted = false;
		Gameover();
	}
	else {
		DrawPreview(&m_shape, m_cfg.colorPreview);
	}

	DrawShape(&m_shape, m_curX, m_curY, m_shape.GetColor());
}

void CTetrisApp::Gameover()
{
	::KillTimer(m_hWnd, ID_TETRIS_APP);
	PlaySnd(IDR_WAVE_GAMEOVER);
}

void CTetrisApp::DrawLevel()
{
	ATL::CString str;
	str.Format("%d", m_level);
	RectF rectF(m_cfg.rectLevel.X, m_cfg.rectLevel.Y, m_cfg.rectLevel.Width, m_cfg.rectLevel.Height);
	DrawTitledBox(rectF, "Level", str);
}

void CTetrisApp::DrawScore()
{
	ATL::CString str;
	str.Format("%d", m_score);
	RectF rectF(m_cfg.rectScore.X, m_cfg.rectScore.Y, m_cfg.rectScore.Width, m_cfg.rectScore.Height);
	DrawTitledBox(rectF, "Score", str);
}

void CTetrisApp::DrawTitledBox(const RectF &rect, ATL::CString title, ATL::CString text)
{
	RectF rectF(rect.X, rect.Y, rect.Width, rect.Height);
	SolidBrush brush(m_cfg.colorBackground);
	m_graphics.FillRectangle(&brush, rectF);

	SolidBrush  solidBrush(m_cfg.colorScore);
	StringFormat stringFormat;
	stringFormat.SetAlignment(StringAlignmentCenter);

	CComBSTR str;
	str = title;
	m_graphics.DrawString(str, -1, m_cfg.pFontTitle, rectF, &stringFormat, &solidBrush);

	str = text;
	RectF rectF2(rect.X, rect.Y+rect.Height/2, rect.Width, rect.Height/2);
	m_graphics.DrawString(str, -1, m_cfg.pFontText, rectF2, &stringFormat, &solidBrush);
}

void CTetrisApp::DrawQueuedShape()
{
	if (m_shapesQ.size() > 0) {
		SolidBrush brush(m_cfg.colorBackground);
		m_graphics.FillRectangle(&brush, m_cfg.rectQueue);

		CShape shape;
		shape.SetType(m_shapesQ.front());

		SolidBrush  solidBrush(m_cfg.colorScore);
		StringFormat stringFormat;
		stringFormat.SetAlignment(StringAlignmentCenter);

		CComBSTR str;
		str = "Preview";

		RectF rectF(m_cfg.rectQueue.X, m_cfg.rectQueue.Y, m_cfg.rectQueue.Width, m_cfg.rectQueue.Height);
		m_graphics.DrawString(str, -1, m_cfg.pFontTitle, rectF, &stringFormat, &solidBrush);

		int itemWidth = 8;
		int itemBoardSize = 1;
		int x = m_cfg.rectQueue.X + (m_cfg.rectQueue.Width - itemWidth * 4) / 2;
		int y = m_cfg.rectQueue.Y + 15;

		brush.SetColor(shape.GetColor());
		DrawShape(&shape, x, y, itemWidth, itemBoardSize, shape.GetColor(), brush);
	}
}

void CTetrisApp::PlaySnd(UINT resID)
{
	LPTSTR snd = MAKEINTRESOURCE(resID);
	::PlaySound(snd, ::GetModuleHandle(NULL), SND_ASYNC | SND_RESOURCE);
}
